package com.zyk.scaffold.oauth.framework.store.redis;

import com.zyk.scaffold.oauth.framework.config.properties.OauthRedisProperties;
import com.zyk.scaffold.oauth.framework.domain.*;
import com.zyk.scaffold.oauth.framework.interfaces.ITokenStore;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import org.springframework.lang.Nullable;

import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RedisTokenStore implements ITokenStore {

    private static final String KEY_SUBJECT = "subject:";
    private static final String KEY_ACCESS = "access:";
    private static final String KEY_REFRESH = "refresh:";
    private static final String KEY_AUTHORIZATION_CODE = "authorization-code:";

    /**
     * String专用
     */
    private RedisTemplate<String, Object> jsonRedisTemplate;

    /**
     * Objecy专用
     */
    private RedisTemplate<String, Object> objectRedisTemplate;
    private String prefix = "oauth";
    private IRedisSerializer redisSerializer = null;
    public RedisTokenStore(OauthRedisProperties redisProperties) {
        initRedis(redisProperties);
        initSerializer(redisProperties.getSerializer());
    }

    private void initSerializer(String serializer) {
        if(OauthRedisProperties.SerializerEnum.MODEL_BITYS.getKey().equals(serializer)){
            redisSerializer = new IRedisSerializer() {
                @Override
                public boolean save(Object obj, String key, Long expiresTime) {
                    objectRedisTemplate.opsForValue().set(key, obj, expiresTime, TimeUnit.SECONDS);
                    return true;
                }

                @Override
                public <T> T get(String key, Class<T> classType) {
                    return (T)objectRedisTemplate.opsForValue().get(key);
                }

                @Override
                public <T> T delete(String key, Class<T> classType) {
                    T t = get(key, classType);
                    objectRedisTemplate.delete(key);
                    return t;
                }
            };
            return;
        }
        redisSerializer = new IRedisSerializer(){
            @Override
            public boolean save(Object obj, String key, Long expiresTime) {
                jsonRedisTemplate.opsForValue().set(key, obj, expiresTime, TimeUnit.SECONDS);
                return true;
            }

            @Override
            public <T> T get(String key, Class<T> classType) {
                return (T)jsonRedisTemplate.opsForValue().get(key);
            }

            @Override
            public <T> T delete(String key, @Nullable Class<T> classType) {
                T t = get(key, classType);
                jsonRedisTemplate.delete(key);
                return t;
            }
        };
    }

    private void initRedis(OauthRedisProperties redisProperties) {
        // 1. Redis配置
        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
        redisConfig.setHostName(redisProperties.getHost());
        redisConfig.setPort(redisProperties.getPort());
        redisConfig.setDatabase(redisProperties.getDatabase());
        redisConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));

        // 2. 连接池配置
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        // pool配置
        OauthRedisProperties.Lettuce lettuce = redisProperties.getLettuce();
        if(lettuce.getPool() != null) {
            OauthRedisProperties.Pool pool = redisProperties.getLettuce().getPool();
            // 连接池最大连接数
            poolConfig.setMaxTotal(pool.getMaxActive());
            // 连接池中的最大空闲连接
            poolConfig.setMaxIdle(pool.getMaxIdle());
            // 连接池中的最小空闲连接
            poolConfig.setMinIdle(pool.getMinIdle());
            // 连接池最大阻塞等待时间（使用负值表示没有限制）
            poolConfig.setMaxWait(Duration.ofMillis(pool.getMaxWait().toMillis()));
        }
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
        // timeout
        if(redisProperties.getTimeout() != null) {
            builder.commandTimeout(redisProperties.getTimeout());
        }
        // shutdownTimeout
        if(lettuce.getShutdownTimeout() != null) {
            builder.shutdownTimeout(lettuce.getShutdownTimeout());
        }
        // 创建Factory对象
        LettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build();
        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisConfig, clientConfig);
        connectionFactory.afterPropertiesSet();
        // 3. 开始初始化
        // 指定相应的序列化方案
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        JdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 构建StringRedisTemplate
        RedisTemplate<String, Object> jsonTemplate = new RedisTemplate<String, Object>();
        jsonTemplate.setConnectionFactory(connectionFactory);
        jsonTemplate.setKeySerializer(keySerializer);
        jsonTemplate.setHashKeySerializer(keySerializer);
        jsonTemplate.setValueSerializer(jsonRedisSerializer);
        jsonTemplate.setHashValueSerializer(jsonRedisSerializer);
        jsonTemplate.afterPropertiesSet();
        // 构建RedisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);
        redisTemplate.afterPropertiesSet();
        this.jsonRedisTemplate = jsonTemplate;
        this.objectRedisTemplate = redisTemplate;
    }
    private String appendKey(String key, String str){
        return new StringBuilder().append(prefix).append(":").append(key).append(str).toString();
    }

    @Override
    public boolean saveRefreshToken(RefreshToken refreshToken) {
        String refreshTokenKey = appendKey(KEY_REFRESH, refreshToken.getRefreshToken());
        return redisSerializer.save(refreshToken, refreshTokenKey, refreshToken.getExpiresTime());
    }

    @Override
    public boolean saveAccessToken(AccessToken accessToken){
        String accessTokenStr = accessToken.getAccessToken();
        String accessTokenKey = appendKey(KEY_ACCESS, accessTokenStr);
        return redisSerializer.save(accessToken, accessTokenKey, accessToken.getExpiresTime());
    }

    @Override
    public String createdAuthorizationCode(AuthorizationCodeDetails authorizationCodeDetails, Long expiresTime) {
        String authorizationCode = RandomStringUtils.randomNumeric(10);
        String authorizationCodeKey = appendKey(KEY_AUTHORIZATION_CODE, authorizationCode);
        redisSerializer.save(authorizationCodeDetails, authorizationCodeKey, expiresTime);
        return authorizationCode;
    }

    @Override
    public AuthorizationCodeDetails validateAuthorizationCode(String authorizationCode) {
        String authorizationCodeKey = appendKey(KEY_AUTHORIZATION_CODE, authorizationCode);
        return redisSerializer.get(authorizationCodeKey, AuthorizationCodeDetails.class);
    }

    @Override
    public RefreshToken getRefreshToken(String refreshToken) {
        String refreshTokenKey = appendKey(KEY_REFRESH, refreshToken);
        return redisSerializer.get(refreshTokenKey, RefreshToken.class);
    }

    @Override
    public AccessToken getAccessToken(String token) {
        String accessTokenKey = appendKey(KEY_ACCESS, token);
        return redisSerializer.get(accessTokenKey, AccessToken.class);
    }

    @Override
    public AccessToken removeToken(String token) {
        String accessTokenKey = appendKey(KEY_ACCESS, token);
        return redisSerializer.delete(accessTokenKey, AccessToken.class);
    }

    @Override
    public RefreshToken removeRefreshToken(String refreshToken) {
        String refreshTokenKey = appendKey(KEY_REFRESH, refreshToken);
        return redisSerializer.delete(refreshTokenKey, RefreshToken.class);
    }

    @Override
    public boolean isExpired(String token) {
        String accessTokenKey = appendKey(KEY_ACCESS, token);
        AccessToken accessToken = redisSerializer.get(accessTokenKey, AccessToken.class);
        return accessToken == null;
    }

    @Override
    public boolean validateToken(String token) {
        return !isExpired(token);
    }

    @Override
    public boolean isExpiredRefresh(String token) {
        String refreshTokenKey = appendKey(KEY_REFRESH, token);
        AccessToken accessToken = redisSerializer.get(refreshTokenKey, AccessToken.class);
        return accessToken == null;
    }

    @Override
    public boolean validateTokenRefresh(String token) {
        return !isExpiredRefresh(token);
    }

    @Override
    public String serializeToken(TokenData tokenData) {
        return UUID.randomUUID().toString();
    }

    @Override
    public Map<String, Object> reverseSerializeToken(String token) {
        String accessTokenKey = appendKey(KEY_ACCESS, token);
        AccessToken accessToken = redisSerializer.get(accessTokenKey, AccessToken.class);
        if(accessToken == null){
            return null;
        }
        return accessToken.toMap();
    }

    @Override
    public String serializeRefreshToken(TokenData tokenData) {
        return UUID.randomUUID().toString();
    }

    @Override
    public Map<String, Object> reverseSerializeRefreshToken(String token) {
        String refreshTokenKey = appendKey(KEY_REFRESH, token);
        AccessToken accessToken = redisSerializer.get(refreshTokenKey, AccessToken.class);
        if(accessToken == null){
            return null;
        }
        return accessToken.toMap();
    }

    @Override
    public boolean saveSubject(Subject subject) {
        return redisSerializer.save(subject, appendKey(KEY_SUBJECT, subject.getSubjectKey()), subject.getExpiresTime());
    }

    @Override
    public Subject getSubject(String subjectKey) {
        return redisSerializer.get(appendKey(KEY_SUBJECT, subjectKey), Subject.class);
    }

    @Override
    public Subject removeSubject(String subjectKey) {
        return redisSerializer.delete(appendKey(KEY_SUBJECT, subjectKey),Subject.class);
    }
}
